<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      let nowWatcher = null;
      function Vue(option) {
        //vue会把配置对象的数据放在实例化对象的_data属性上
        this._data = option.data;

        //数据代理:把所有的数据中的属性在实例化对象上放一份
        //但是当设置实例化对象的属性时,其实设置的是_data中的值
        //当获取实例化对象的属性是,其实获取的是_data中的值
        for (let key in this._data) {
          //不能直接给实例化扩展属性并直接设置值
          // this[key] = this._data[key];
          //我们应该给实例化对象设置getter和setter存取器属性进行代理
          Object.defineProperty(this, key, {
            get() {
              return this._data[key];
            },
            set(value) {
              this._data[key] = value;
            },
          });
        }

        //开始数据劫持的流程
        observe(this._data);
      }

      //封装一个observe函数,用来遍历对象判断是否需要数据劫持,并进行数据劫持
      function observe(obj) {
        // 判断obj如果不是对象类型,则直接return出去即可
        if (typeof obj !== "object" || obj === null) return;

        //如果当前observe接受的值是一个对象,则需要开始劫持(实例化一个发布者)
        new Observer(obj);
      }

      //发布订阅模式的--发布者
      class Observer {
        constructor(obj) {
          //判断当前的obj是否是一个数组,如果是数组,则对数组遍历,把数组的值进行observe判断劫持
          if (Array.isArray(obj)) {
            this.observeArray(obj);
          } else {
            //如果当前的值不是数组,则是一个对象,需要对对象内的每一个属性进行数据劫持
            this.walk(obj);
          }
        }

        //处理数组的方法
        observeArray(obj) {
          obj.forEach((item) => {
            observe(item);
          });
        }

        //封装一个数据劫持的方法
        walk(obj) {
          for (let key in obj) {
            let value = obj[key];

            //遍历数据的时候,我们要对属性值进行再次递归调用observe,进行判断劫持
            observe(value);

            //对当前遍历出来的key进行数据劫持
            this.defineReactive(obj, key, value);
          }
        }

        //封装一个处理数据劫持的核心方法
        defineReactive(obj, key, value) {
          //实例化一个订阅中心
          let dep = new Dep();

          //数据劫持
          Object.defineProperty(obj, key, {
            get() {
              console.log("Observer--有人来拿值了");

              //让当前数据对应的订阅中心收集依赖
              //通过全局变量nowWatcher是否有值判断当前是否是watcher来读值,决定是否收集依赖
              console.log(nowWatcher);
              if (nowWatcher) {
                dep.depend();
              }

              return value;
            },
            set(val) {
              //判断当前设置的新值和旧值是否一样,如果是,则不再触发更新
              if (val === value) return;
              //重新赋值
              value = val;
              //当val这个新值被设置进来的时候,需要对val这个值再次进行observe判断劫持
              observe(val);

              //当前数据更新之后,让当前数据的订阅中心通知所有的依赖更新
              dep.notify();

              console.log("Observer--有人设置新的值了");
            },
          });
        }
      }

      //发布订阅模式的--订阅中心
      class Dep {
        constructor() {
          //定义一个数组用来收集依赖
          this.subs = [];
        }

        //封装一个收集依赖的方法
        depend() {
          console.log("Dep---收集依赖");
          //因为在收集的时候,全局变量保存的是当前的watcher,我们把当前的watcher收集起来
          this.subs.push(nowWatcher);
        }

        //封装一个通知所有依赖更新的方法
        notify() {
          console.log("Dep---通知所有的依赖更新");
          //一旦需要通知watcher更新,则编译subs中所有的watcher,调用wather身上的update方法进行更新
          this.subs.forEach((item) => {
            item.update();
          });
        }
      }

      //发布订阅模式的--订阅者类
      class Watcher {
        constructor(obj, key) {
          this.obj = obj;
          this.key = key;

          //因为只有watcher读值的时候,才收集依赖
          //所以在watcher读值的时候,我们把当前的watcher实例保存在全局的变量上,让收集的时候进行判断
          nowWatcher = this;
          this.get();
          //一旦上边获取值完成,我们要把全局的变量清空,防止下一次误收集
          nowWatcher = null;
        }

        //watcher读取值的方法
        get() {
          console.log("Watcher--get取值的方法");
          return this.obj[this.key];
        }

        update() {
          console.log(
            `我当前读取的obj中的${this.key}属性发生了改变,我接到了通知去读取新的值,然后重新渲染`
          );
        }
      }

      const vm = new Vue({
        data: {
          count: 1,
          price: 100,
          score: {
            en: 90,
            ch: 100,
            math: 120,
          },
        },
      });
      console.log(vm);

      new Watcher(vm, "count");
    </script>
  </body>
</html>
